#include <pthread.h>

#include "cache.h"
#include "parameters.h"

void create_meta_data(meta_data **new_m)
{
    *new_m = (meta_data *) malloc(sizeof(meta_data));
    (*new_m)->access = 1;
    (*new_m)->file_name = NULL;
    (*new_m)->last_access = (struct tm*) malloc(sizeof(struct tm));
    (*new_m)->came_from_server = (struct tm*) malloc(sizeof(struct tm));
    (*new_m)->request = NULL;
    (*new_m)->size = 0;
}

void init_from_file(meta_data **data,vstring *line)
{
    vstring *data_path = NULL,*value = NULL;
    get_token(&line,&((*data)->file_name),SEMICOLON);
    get_token(&line,&((*data)->request),SEMICOLON);

    get_token(&line,&value,SEMICOLON);
    string_to_integer(value,&((*data)->access));

    get_token(&line,&value,SEMICOLON);
    (*data)->last_access = get_date(&value);

    get_token(&line,&value,SEMICOLON);
    (*data)->came_from_server = get_date(&value);

    path_file_cache(&data_path,(*data)->file_name);
    (*data)->size = get_file_size(data_path);
}

void create_level1(level1 **new_l)
{
    *new_l = (level1 *) malloc(sizeof(level1));
    (*new_l)->data = NULL;
    (*new_l)->file_data = NULL;
}

void print_file(meta_data *f)
{
    printf("access: %4i\t",f->access);
    printf("%-10s ",f->file_name->pt);
    printf("%s\t",f->request->pt);
    printf("%i\n",f->size);
}

int left_son(int index,int last)
{
    if((2 * index) <= last)
        return (2 * index);
    return 0;
}

int right_son(int index,int last)
{
    if(((2 * index) + 1) <= last)
        return ((2 * index) + 1);
    return 0;
}

int get_father(int index)
{
    if(index / 2 >= 1)
        return index / 2;
    return 0;
}

int minimum(int a, int b)
{
    if(a < b)
        return 1;
    return 0;
}

int maximum(int a, int b)
{
    if(a > b)
        return 1;
    return 0;
}

/*******************************************************************************
 *                      FUNÇÕES PARA CACHE NÍVEL1
 ******************************************************************************/

void print_level1()
{
    heap_level1 *h = &(memory.cache_data_l1);

    int i;
    printf("total: %i\n",h->length);
    printf("occuped: %i\n",h->last);
    printf("¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨\n");
    for(i = 1; i <= h->last; i++)
        print_file(h->elements[i]->data);
    printf("¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨\n\n");
}

void swap_level1(int a, int b)
{
    heap_level1 *h = &(memory.cache_data_l1);

    level1 *temp;
    temp = h->elements[a];
    h->elements[a] = h->elements[b];
    h->elements[b] = temp;
}

void heap_down_level1(int index)
{
    int left,right,min;
    heap_level1 *h = &(memory.cache_data_l1);

    min = index;

    left = left_son(index,h->last);
    right = right_son(index,h->last);

    if(right && minimum(h->elements[right]->data->access,h->elements[min]->data->access))
        min = right;
    if(left && minimum(h->elements[left]->data->access,h->elements[min]->data->access))
        min = left;

    if(min != index)
    {
        swap_level1(index,min);
        heap_down_level1(min);
    }
}

void heap_up_level1(int index)
{
    int father;
    heap_level1 *h = &(memory.cache_data_l1);

    father = get_father(index);
    if(father && minimum(h->elements[index]->data->access,h->elements[father]->data->access))
    {
        swap_level1(index,father);
        heap_up_level1(father);
    }
}

void heapfy_level1()
{
    int i;
    heap_level1 *h = &(memory.cache_data_l1);

    for(i = h->last / 2; i > 0; i--)
        heap_down_level1(i);
}

void remove_heap_level1(int index)
{
    vstring *path = NULL;
    heap_level1 *h = &(memory.cache_data_l1);

    swap_level1(index,h->last);
    memory.occuped_l1 -= h->elements[h->last]->data->size;

    path_file_cache(&path,h->elements[index]->data->file_name);
    write_file(path,h->elements[index]->file_data);
    h->elements[h->last]->data = NULL;
    free(h->elements[h->last]);
    h->last--;
    heap_down_level1(index);
}

void resize_level1()
{
    int i;
    level1 **aux;

    memory.cache_data_l1.length = memory.cache_data_l1.length * 2;
    aux = (level1 **) malloc(memory.cache_data_l1.length * sizeof(level1*));
    for(i = 1; i <= memory.cache_data_l1.last; i++)
        aux[i] = (memory.cache_data_l1.elements[i]);
    free(memory.cache_data_l1.elements);
    memory.cache_data_l1.elements = aux;
}

void insert_level1(level1 **file)
{
    while(memory.size_l1 - memory.occuped_l1 < (*file)->data->size)
        remove_heap_level1(1);

    // inserção do arquivo no nível1
    memory.occuped_l1 += (*file)->data->size;
    // incrementamos a quantidade de arquivos no vetor da heap
    memory.cache_data_l1.last++;
    // se o vetor está cheio, dobramos o tamanho dele
    if(memory.cache_data_l1.last == memory.cache_data_l1.length)
        resize_level1();

    // inserimos o arquivo novo no final do vetor da heap e fazemos o heap_up
    memory.cache_data_l1.elements[memory.cache_data_l1.last] = *file;
    heap_up_level1(memory.cache_data_l1.last);
}

/*******************************************************************************
 *                      FUNÇÕES PARA CACHE NÍVEL2
 ******************************************************************************/

void print_level2()
{
    heap_level2 *h = &(memory.cache_data_l2);

    int i;
    printf("total: %i\n",h->length);
    printf("occuped: %i\n",h->last);
    printf("¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨\n");
    for(i = 1; i <= h->last; i++)
        print_file(h->elements[i]);
    printf("¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨\n\n");
}

void swap_level2(int a, int b)
{
    heap_level2 *h = &(memory.cache_data_l2);

    meta_data *temp;
    temp = h->elements[a];
    h->elements[a] = h->elements[b];
    h->elements[b] = temp;
}

void heap_down_level2(int index)
{
    int left,right,min;
    heap_level2 *h = &(memory.cache_data_l2);

    min = index;

    left = left_son(index,h->last);
    right = right_son(index,h->last);

    if(right && minimum(h->elements[right]->access,h->elements[min]->access))
        min = right;
    if(left && minimum(h->elements[left]->access,h->elements[min]->access))
        min = left;

    if(min != index)
    {
        swap_level2(index,min);
        heap_down_level2(min);
    }
}

void heap_up_level2(int index)
{
    int father;
    heap_level2 *h = &(memory.cache_data_l2);

    father = get_father(index);
    if(father && minimum(h->elements[index]->access,h->elements[father]->access))
    {
        swap_level2(index,father);
        heap_up_level2(father);
    }
}

void heapfy_level2()
{
    int i;
    heap_level2 *h = &(memory.cache_data_l2);

    for(i = h->last / 2; i > 0; i--)
        heap_down_level2(i);
}

void remove_heap_level2(int index)
{
    int ind_level1 = 0;
    vstring *path = NULL;
    heap_level2 *h = &(memory.cache_data_l2);

    path_file_cache(&path,h->elements[index]->file_name);
    remove(path->pt);
    ind_level1 = search_request_level1(h->elements[index]->request);

    if(ind_level1 != 0)
        remove_heap_level1(ind_level1);

    swap_level2(index,h->last);
    memory.occuped_l2 -= h->elements[h->last]->size;
    free(h->elements[h->last]);
    h->last--;
    heap_down_level2(index);
}

void resize_level2()
{
    int i;
    meta_data **aux;

    memory.cache_data_l2.length = memory.cache_data_l2.length * 2;
    aux = (meta_data **) malloc(memory.cache_data_l2.length * sizeof(meta_data*));
    for(i = 1; i <= memory.cache_data_l2.last; i++)
        aux[i] = (memory.cache_data_l2.elements[i]);
    free(memory.cache_data_l2.elements);
    memory.cache_data_l2.elements = aux;
}

void insert_level2(level1 **file)
{
    vstring *path = NULL;
    meta_data *meta_aux = (*file)->data;
    
    while(memory.size_l2 - memory.occuped_l2 < meta_aux->size)
        remove_heap_level2(1);
    path_file_cache(&path,meta_aux->file_name);
    write_file(path,(*file)->file_data);
    // inserção do arquivo no nível1
    memory.occuped_l2 += meta_aux->size;
    // incrementamos a quantidade de arquivos no vetor da heap
    memory.cache_data_l2.last++;
    // se o vetor está cheio, dobramos o tamanho dele
    if(memory.cache_data_l2.last == memory.cache_data_l2.length)
        resize_level2();

    // inserimos o arquivo novo no final do vetor da heap e fazemos o heap_up
    memory.cache_data_l2.elements[memory.cache_data_l2.last] = meta_aux;
    heap_up_level2(memory.cache_data_l2.last);
}

void sort_level2()
{
    int n = memory.cache_data_l2.last;

    while(memory.cache_data_l2.last > 1)
    {
        swap_level2(1,memory.cache_data_l2.last);
        memory.cache_data_l2.last--;
        heap_down_level2(1);
    }
    memory.cache_data_l2.last = n;
}
/*******************************************************************************
 *                          FUNÇÕES DE MANIPULAÇÃO DA CACHE
 ******************************************************************************/

void print_cache()
{
    printf("----------------------- CACHE ----------------------\n");
    printf("\t\tLEVEL1\n");
    printf("size level1: %i Bytes\n",memory.size_l1);
    printf("occuped: %i Bytes\n",memory.occuped_l1);
    print_level1();
    printf("\t\tLEVEL2\n");
    printf("size level2: %i Bytes\n",memory.size_l2);
    printf("occuped: %i Bytes\n",memory.occuped_l2);
    print_level2();
    printf("----------------------------------------------------\n");
}

void new_cache()
{
    pthread_rwlockattr_init(&cache_attr);
    pthread_rwlockattr_setkind_np(&cache_attr,PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
    pthread_rwlock_init(&cache_semaphore,&cache_attr);    
    
    memory.size_l1 = configure.cache_size_l1 * MEGA_BYTE;
    memory.size_l2 = configure.cache_size_l2 * MEGA_BYTE;
    memory.occuped_l1 = 0;
    memory.occuped_l2 = 0;
    memory.cache_data_l1.last = 0;
    memory.cache_data_l2.last = 0;
    memory.cache_data_l1.length = INIT_SIZE_L1; // quantidade inicial qualquer
    memory.cache_data_l2.length = INIT_SIZE_L2; // quantidade inicial qualquer
    memory.cache_data_l1.elements = (level1 **) malloc((INIT_SIZE_L1 + 1) * sizeof(level1*));
    memory.cache_data_l2.elements = (meta_data **) malloc((INIT_SIZE_L2 + 1) * sizeof(meta_data*));
}

void create_name_file(vstring **file)
{
    int num = 0;
    vstring *path = NULL;
    FILE *f;
    num = get_random_number(memory.size_l2);
    integer_to_string(file,num);
    path_file_cache(&path,*file);
    while((f = fopen(path->pt,"r")) != NULL) // verificando se o nome já existe
    {
        fclose(f);
        free_string(&path);
        num = get_random_number(memory.size_l2);
        integer_to_string(file,num);
        path_file_cache(&path,*file);
    }
}

void path_file_cache(vstring **path,vstring *file)
{
    copy_vstring(path,configure.cache_dir);
    if((*path)->pt[(*path)->length - 1] != '/')
        concat_string(path,SLASH);
    concat_vstring(path,file);
}

void line_cache_file(meta_data **data,vstring **line)
{
    vstring *aux = NULL;
    
    copy_vstring(line,(*data)->file_name);
    concat_string(line,SEMICOLON);
    concat_vstring(line,(*data)->request);
    concat_string(line,SEMICOLON);
    
    integer_to_string(&aux,(*data)->access);
    concat_vstring(line,aux);
    concat_string(line,SEMICOLON);

    concat_string(line,asctime((*data)->last_access));
    replace_occour(line,END_LINE,SEMICOLON);

    concat_string(line,asctime((*data)->came_from_server));
    replace_occour(line,END_LINE,SEMICOLON);

    concat_string(line,END_LINE);
    
    free_string(&aux);
}

void init_cache()
{
    int i = 1,j;
    vstring *data_path = NULL,*cache_path = NULL,*cache_aux = NULL,*cache_f = NULL;
    vstring *data_aux = NULL,*line = NULL;
    meta_data *meta_aux;
    level1 *level_aux;

    copy_string(&cache_f,CACHE_FILE);
    path_file_cache(&cache_path,cache_f);
    if(read_file(cache_path,&cache_aux))
    {
        // percorre o arquivo da cache recolhendo os meta dados e inicializando
        // a cache de nível 2
        while(get_line(&line,&cache_aux))
        {
            // cria o data_file para ser inserido na heap
            create_meta_data(&meta_aux);
            init_from_file(&meta_aux,line);
            // aumentamos o espaço de memória ocupada da cache
            memory.occuped_l2 += meta_aux->size;
            // inserimos o arquivo na heap da cache
            memory.cache_data_l2.elements[i] = meta_aux;
            // incrementamos o contador de elementos da heap
            memory.cache_data_l2.last = i;
            // se o vetor da heap está cheio, dobramos o tamanho dele
            if(i == memory.cache_data_l2.length)
                resize_level2();
            i++;
            free_string(&line);
        }
        // percorre a cache nível 2 de trás para frente copiando os arquivos com
        // mais acessos para a cache de nível 1 e lendo o conteúdo destes
        // arquivos
        j = memory.cache_data_l2.last;
        i = 1;
        while(j > 0 && memory.occuped_l1 < memory.size_l1)
        {
            meta_aux = memory.cache_data_l2.elements[j];
            if(memory.occuped_l1 + meta_aux->size < memory.size_l1)
            {
                // usa o nome do arquivo para criar um caminho até o diretório da cache para abri-lo
                path_file_cache(&data_path,meta_aux->file_name);
                // tenta abrir o arquivo e recuperar seu conteúdo
                if(read_file(data_path,&data_aux))
                {
                    create_level1(&level_aux);
                    level_aux->data = meta_aux;
                    copy_vstring(&(level_aux->file_data),data_aux);                    
                    // aumentamos o espaço de memória ocupada da cache
                    memory.occuped_l1 += level_aux->data->size;
                    //printf("memória ocupada: %i\n",memory.occuped_l1);
                    // inserimos o arquivo na heap da cache
                    memory.cache_data_l1.elements[i] = level_aux;
                    // incrementamos o contador de elementos da heap
                    memory.cache_data_l1.last = i;
                    // se o vetor da heap está cheio, dobramos o tamanho dele
                    if(i == memory.cache_data_l1.length)
                        resize_level1();
                    i++;
                }
            }
            j--;
        }
        // "heapfica" porque os arquivos estão em ordem decrescente de acessos
        heapfy_level1();
        heapfy_level2();
    }
}

int search_request_level1(vstring *request)
{
    int i,found = 0;
    if(memory.cache_data_l1.last)
        for(i = 1; i <= memory.cache_data_l1.last && !found; i++)
            if(strcmp(request->pt,memory.cache_data_l1.elements[i]->data->request->pt) == 0)
                found = i;
    return found;
}

int search_request_level2(vstring *request)
{
    int i,found = 0;
    if(memory.cache_data_l2.last)
        for(i = 1; i <= memory.cache_data_l2.last && !found; i++)
            if(strcmp(request->pt,memory.cache_data_l2.elements[i]->request->pt) == 0)
                found = i;
    return found;
}

void insert_in_cache(level1 **file)
{
    if(memory.size_l2 > (*file)->data->size)
    {
        printf("inserindo no nível2\n\n");
        insert_level2(file);
        if(memory.size_l1 > (*file)->data->size)
        {
            printf("inserindo nível1\n\n");
            insert_level1(file);
        }
    }
    else
    {
        printf("O tamanho da cache é insuficiente para guardar este arquivo\n\n");
        free(*file);
    }
}

void close_cache()
{
    int i,last;
    vstring *line = NULL,*file = NULL,*path = NULL;
    vstring *path_cache = NULL,*cache_file = NULL;
    level1 *aux = NULL;

    copy_string(&cache_file,CACHE_FILE);
    last = memory.cache_data_l2.last;

    if(last)
    {
        path_file_cache(&path_cache,cache_file);
        sort_level2();

        line_cache_file(&(memory.cache_data_l2.elements[last]),&file);
        // criação de uma linha de meta dados para cada item da cache de nível2
        for(i = last - 1; i > 0; i--)
        {
            line_cache_file(&(memory.cache_data_l2.elements[i]),&line);
            concat_vstring(&file,line);
            free_string(&line);
        }
        // persistência dos arquivos do nível1 no diretório da cache
        for(i = 1; i <= memory.cache_data_l1.last; i++)
        {
            aux = memory.cache_data_l1.elements[i];
            path_file_cache(&path,aux->data->file_name);
            write_file(path,aux->file_data);
        }
        // escrita do init_cache.txt
        write_file(path_cache,file);
    }
}

int hit_level1(int index,vstring **body)
{
    int body_size;
    time_t now = time(NULL);
    memory.cache_data_l1.elements[index]->data->access++;
    body_size = memory.cache_data_l1.elements[index]->data->size;
    memory.cache_data_l1.elements[index]->data->last_access = localtime(&now);
    copy_vstring(body,memory.cache_data_l1.elements[index]->file_data);
    heap_down_level1(index);
    heapfy_level2();
    
    return body_size;
}

int hit_level2(int index,vstring **body)
{
    int body_size;
    vstring *path = NULL;
    level1 *lv1 = NULL;
    meta_data *lv2 = NULL;
    time_t now = time(NULL);

    lv2 = memory.cache_data_l2.elements[index];
    lv2->last_access = localtime(&now);

    path_file_cache(&path,lv2->file_name);

    read_file(path,body);

    lv2->access++;
    body_size = lv2->size;

    if(body_size + memory.occuped_l1 < memory.size_l1)
    {
        create_level1(&lv1);
        lv1->data = lv2;
        copy_vstring(&(lv1->file_data),*body);
        insert_level1(&lv1);
    }

    heap_down_level2(index);

    return body_size;
}